Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

30장. 외부 연동과 복원력 — Timeout · Retry · 멱등성 · Circuit Breaker

Agent는 happy path를 아주 잘 만든다.

fun pay(request: PayRequest): PayResult {
    val response = pgClient.request(request)
    return PayResult.from(response)
}

깔끔하다.

그리고 외부 시스템은 이 코드가 가정한 대로
동작하지 않는다.


반드시 물어야 하는 네 가지

외부 연동 코드를 볼 때마다 던지는 질문이다.

질문안 물으면
타임아웃은 몇 초인가무한 대기
재시도하는가일시 오류가 최종 실패
두 번 호출되면 어떻게 되는가중복 결제
실패하면 우리 상태는 어떻게 되는가정합성 붕괴

🔥 세 번째와 네 번째가 백엔드의 진짜 문제다.

앞의 둘은 설정이지만
뒤의 둘은 설계다.

Agent는 앞의 둘을 잘 붙이고
뒤의 둘은 묻지 않으면 다루지 않는다.


타임아웃 없는 코드를 찾는다

Agent가 잘하는 정적 확인 작업이다.

외부 시스템을 호출하는 코드를 전부 찾아줘.

- HTTP 클라이언트, 메시지 발행, Redis, 파일 저장소 전부
- 각각의 연결 타임아웃과 읽기 타임아웃 설정값을 표로 만들어줘
- 설정이 없는 곳은 "미설정" 으로 표시해줘

수정은 하지 마.

레거시에서 이 표를 처음 뽑으면
대개 절반이 “미설정” 이다.

기본값이 무한대인 클라이언트가 섞여 있다.


재시도의 전제는 멱등성이다

여기가 이 장에서 가장 중요하다.

재시도 = 같은 요청을 다시 보낸다

받는 쪽이 두 번 처리하면
재시도는 개선이 아니라 사고다.

⚠️ Agent는 이 순서를 뒤집는다.

@Retryable 을 먼저 붙이고,
멱등성은 언급하지 않는다.

지시로 순서를 고정한다.

PG 호출에 재시도를 추가하려고 해.

먼저 확인해줘:
- 이 API가 멱등한가 (같은 요청 두 번 = 한 번과 같은가)
- 멱등키를 보내고 있는가
- PG 문서상 재시도 안전한 응답 코드는 무엇인가

멱등성이 보장되지 않으면 재시도를 붙이지 말고 알려줘.

무엇을 재시도하고 무엇을 안 하는가

재시도재시도 금지
연결 실패잔액 부족
읽기 타임아웃카드 한도 초과
5xx유효하지 않은 카드
429 (백오프 후)인증 실패

⚠️ 타임아웃이 까다롭다.

요청은 도달했는데 응답만 못 받았을 수 있다.
저쪽에서는 성공한 상태다.

그래서 타임아웃 재시도는
멱등키가 있을 때만 안전하다.

이 판단을 규칙으로 적어둔다.

## 외부 연동

- 재시도는 멱등키가 있는 요청에만 적용한다
- 4xx 는 재시도하지 않는다 (429 제외)
- 재시도는 최대 3회, 지수 백오프
- 타임아웃은 반드시 명시한다 (기본값에 의존하지 않는다)
- 외부 호출은 트랜잭션 밖에서 한다 (28장)

실패했을 때 우리 상태

Agent가 가장 자주 빠뜨리는 부분이다.

@Transactional
fun pay(request: PayRequest) {
    val payment = paymentRepository.save(Payment.pending(request))
    val result = pgClient.request(request)   // 여기서 타임아웃
    payment.complete(result)                 // 실행 안 됨
}

paymentPENDING 으로 남는다.

그런데 PG 쪽에서는 결제가 성공했을 수 있다.

이 불일치를 어떻게 해소하는가.

외부 호출이 타임아웃됐을 때
우리 데이터가 어떤 상태로 남는지 확인해줘.

- 저쪽은 성공했는데 우리는 모르는 경우가 가능한가
- 그 상태를 나중에 어떻게 확인하는가 (조회 API가 있는가)
- 그 확인은 누가 언제 하는가 (배치? 사용자 재조회?)

세 번째 질문의 답이 없으면
그 데이터는 영원히 애매한 상태로 남는다.

대개 답은 정합성 배치다.
그리고 그 배치를 만드는 것까지가 이 작업의 범위다.


Circuit Breaker

외부가 죽었을 때 우리까지 죽지 않게 한다.

Agent는 이것을 잘 붙인다.
설정값만 정해주면 된다.

- 실패율 50% 이상, 최소 20건 → Open
- Open 유지 30초 → Half-Open
- 폴백: 결제는 폴백 없음(즉시 실패), 조회는 캐시된 값

⚠️ 폴백 정책이 도메인 판단이다.

조회는 옛 데이터를 주는 편이 나을 수 있지만
결제는 절대 아니다.

Agent에게 맡기면 “안전하게” 폴백을 넣는다.


실패를 테스트한다

30장의 완료 조건은 실패 시나리오로 채워진다.

@Test
fun `PG 타임아웃 시 3회까지 재시도한다`() {
    mockPg.enqueue(MockResponse().setSocketPolicy(NO_RESPONSE))
    ...
}

@Test
fun `잔액 부족 응답은 재시도하지 않는다`() { ... }

@Test
fun `같은 멱등키로 재시도해도 결제는 1건만 생성된다`() { ... }

@Test
fun `PG 장애 시 서킷이 열려 즉시 실패한다`() { ... }

MockWebServer나 WireMock으로
지연·에러·연결 끊김을 주입한다.

🔥 실패 주입 테스트가 없으면
복원력 코드는 검증된 적 없는 코드다.


실제 호출은 막는다

7장의 금지 목록에 반드시 들어간다.

- 외부 PG·알림·메시지 API를 실제로 호출하지 않는다
- 테스트는 MockPgClient 또는 MockWebServer 로만 한다
- 샌드박스 환경 호출도 사람이 수동으로만 한다

⚠️ 알림 API가 특히 위험하다.

Agent가 테스트하다가
실제 고객에게 문자가 나간다.

되돌릴 수 없는 일이다.


이 장의 핵심

  • Agent는 happy path를 잘 만들고 외부 시스템은 그대로 동작하지 않는다
  • 타임아웃·재시도는 설정이고, 멱등성·실패 후 상태는 설계다
  • Agent는 설정은 잘 붙이고 설계는 묻지 않으면 다루지 않는다
  • 타임아웃 미설정 코드를 찾는 정적 확인은 Agent가 빠르다
  • 재시도의 전제는 멱등성이다 — 순서를 지시로 고정한다
  • 타임아웃은 요청이 도달했을 수 있어서 멱등키 없이는 재시도가 위험하다
  • 실패 후 우리 상태가 애매하게 남는지, 누가 언제 해소하는지까지가 작업 범위다
  • 폴백 정책은 도메인 판단이다 — 조회와 결제의 답이 다르다
  • 실패 주입 테스트가 없으면 복원력 코드는 검증된 적 없는 코드다
  • 알림 API 호출은 되돌릴 수 없다 — 권한으로 막는다